// License Notification
//======================================================
// Copyright (c) 2010 Channel Intelligence Inc.
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// ======================================================




using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using CIP4.Common;

namespace CIP4.RulesEngine
{
    public class DateTimeExpressionType : CIP4.RulesEngine.ExpressionType
    {
        public static DateTimeOperatorsEnum ArithmeticOperatorsEnumFromString(string value)
        {
            if (string.Compare(value, "ADDDAYS", true) == 0)
                return DateTimeOperatorsEnum.ADDDAYS;
            else if (string.Compare(value, "ADDHOURS", true) == 0)
                return DateTimeOperatorsEnum.ADDHOURS;
            else if (string.Compare(value, "ADDMINUTES", true) == 0)
                return DateTimeOperatorsEnum.ADDMINUTES;
            else if (string.Compare(value, "ADDMINUTES", true) == 0)
                return DateTimeOperatorsEnum.ADDMINUTES;
            else if (string.Compare(value, "ADDYEARS", true) == 0)
                return DateTimeOperatorsEnum.ADDYEARS;
            else
                throw new ArgumentException(string.Format("{0} is not a valid ArithmeticOperatorEnum value", value));
        }

        DateTimeOperatorsEnum m_ExpressionOperator;

        public DateTimeOperatorsEnum ExpressionOperator
        {
            get { return m_ExpressionOperator; }
            set { m_ExpressionOperator = value; }
        }

        public override CIP4PrimitiveType UnderlyingType
        {
            get { return CIP4PrimitiveType.CIP4DateTime; }
        }

        public DateTimeExpressionType(DateTimeOperatorsEnum expressionOperator, List<CIValueType> expressionOperands)
        {
            m_ExpressionOperator = expressionOperator;
            Operands.SetOperands(expressionOperands);
        }

        public DateTimeExpressionType(XElement element)
            : base(element)
        {
            XAttribute operatorAtt = element.Attribute("Operator");
            if (operatorAtt != null)
                ExpressionOperator = ArithmeticOperatorsEnumFromString(operatorAtt.Value);
        }

        public override void XmlFor(XmlWriter xwrtr, bool writeNamespace)
        {
            xwrtr.WriteStartElement("dateTimeExpression");
            if (writeNamespace)
                xwrtr.WriteAttributeString("xmlns", RuleXmlNS);
            writeNamespace = false;
            xwrtr.WriteAttributeString("Operator", ExpressionOperator.ToString());
            foreach (CIValueType val in Operands)
                val.XmlFor(xwrtr, writeNamespace);
            xwrtr.WriteEndElement();
        }

        public override object Evaluate(IDataPacketType data)
        {
            if (data != null && data.PacketStats != null)
                data.PacketStats.DateTimeOperatorsCount[(int)ExpressionOperator] += 1;
            object op1 = Operands[0].Evaluate(data);
            object op2 = Operands[1].Evaluate(data);
            if (!(Operands[0] is DateTimeExpressionType || Operands[0] is DateTimeAtomType))
                throw new ApplicationException("unexpected addays operand encountered: working on " + ToString());
            if (!(Operands[1] is ArithmeticAtomType || Operands[1] is ArithmeticExpressionType))
                throw new ApplicationException("unexpected add operand encountered: working on " + ToString());
            switch (ExpressionOperator)
            {
                case DateTimeOperatorsEnum.ADDDAYS:
                    return ((DateTime)op1).AddDays(System.Convert.ToDouble(op2));
                case DateTimeOperatorsEnum.ADDHOURS:
                    return ((DateTime)op1).AddHours(System.Convert.ToDouble(op2));
                case DateTimeOperatorsEnum.ADDMINUTES:
                    return ((DateTime)op1).AddMinutes(System.Convert.ToDouble(op2));
                case DateTimeOperatorsEnum.ADDMONTHS:
                    return ((DateTime)op1).AddMonths(System.Convert.ToInt32(op2));
                case DateTimeOperatorsEnum.ADDYEARS:
                    return ((DateTime)op1).AddYears(System.Convert.ToInt32(op2));
                default:
                    throw new ApplicationException("unexpected datetime operator type encountered: working on " + ToString());
            }
        }

        public override bool Check(IDataPacketType data)
        {
            bool result = true;

            LoggerDelegate localLogger = delegate(MajorEnum status, MinorEnum minorStatus, string message, object[] paras)
            {
                if (status == MajorEnum.Error)
                    result = false;
                return data.Logger(status, minorStatus, message, paras);
            };

            switch (ExpressionOperator)
            {
                case DateTimeOperatorsEnum.ADDDAYS:
                case DateTimeOperatorsEnum.ADDHOURS:
                case DateTimeOperatorsEnum.ADDMINUTES:
                case DateTimeOperatorsEnum.ADDMONTHS:
                case DateTimeOperatorsEnum.ADDYEARS:
                    {
                        if (Operands.Count > 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, "A logical expression using the {0} operator cannot have more than two operands: {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else if (Operands.Count < 2)
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, "A logical expression using the {0} operator must have exactly two operands: {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else if (!((Operands[0] is DateTimeAtomType || Operands[0] is DateTimeExpressionType) &&
                            (Operands[1] is IntegerAtomType || Operands[1] is ArithmeticExpressionType)))
                            localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, "The first operand for the {0} operator must be a dateTime the second operand must be an arithmetic expression or an Integer: {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        else
                        {
                            Operands[0].Check(data);
                            Operands[1].Check(data);
                        }
                        break;
                    }
                default:
                    {
                        localLogger(MajorEnum.Error, MinorEnum.ExpressionStructure, "{0} is not a recognized arithmetic operator: {1}", new object[] { ExpressionOperator.ToString(), this.ToString() });
                        break;
                    }
            }
            return result;
        }

        public override string ToString(ExpressionFormatEnum format)
        {
            switch (format)
            {
                case ExpressionFormatEnum.javascript:
                    return ToJavaScriptString();
                default:
                    return ToString();
            }
        }

        public override string ToString()
        {
            string dateString = Operands.Count > 0 ? Operands[0].ToString() : "InvalidOperand";
            string intervalString = Operands.Count > 1 ? Operands[1].ToString() : "InvalidOperand";
            switch (ExpressionOperator)
            {
                case DateTimeOperatorsEnum.ADDDAYS:
                    // DateAdd("d", number of days, date)
                    return "DateAdd(\"d\", " + intervalString + ", " + dateString + ")";
                case DateTimeOperatorsEnum.ADDHOURS:
                    // DateAdd("h", number of days, date)
                    return "DateAdd(\"h\", " + intervalString + ", " + dateString + ")";
                case DateTimeOperatorsEnum.ADDMINUTES:
                    // DateAdd("n", number of days, date)
                    return "DateAdd(\"n\", " + intervalString + ", " + dateString + ")";
                case DateTimeOperatorsEnum.ADDMONTHS:
                    // DateAdd("m", number of days, date)
                    return "DateAdd(\"m\", " + intervalString + ", " + dateString + ")";
                case DateTimeOperatorsEnum.ADDYEARS:
                    // DateAdd("yyyy", number of days, date)
                    return "DateAdd(\"yyyy\", " + intervalString + ", " + dateString + ")";
                default:
                    return "DateAdd(\"InvalidOperand\", " + intervalString + ", " + dateString + ")";
            }
        }

        private string ToJavaScriptString()
        {
            if (Operands.Count != 2)
                throw new InvalidExpressionException(Operands.Count + "operands for DateTimeExpressionType.");
            string dateString = Operands[0].ToString(ExpressionFormatEnum.javascript);
            string intervalString = Operands[1].ToString(ExpressionFormatEnum.javascript);
            switch (ExpressionOperator)
            {
                case DateTimeOperatorsEnum.ADDDAYS:
                    return "(" + dateString + ")+" + intervalString;
                case DateTimeOperatorsEnum.ADDHOURS:
                    return "(" + dateString + ").valueOf()+(3600000*(" + intervalString + "))";
                case DateTimeOperatorsEnum.ADDMINUTES:
                    return "(" + dateString + ").valueOf()+(60000*(" + intervalString + "))";
                case DateTimeOperatorsEnum.ADDMONTHS:
                    // no javascript equivalent
                    throw new InvalidExpressionException("Unhandled operator: ADDMONTHS");
                case DateTimeOperatorsEnum.ADDYEARS:
                    // TODO: long output, double sending dateString
                    return "(" + dateString + ").setFullYear((" + dateString + ").getFullYear()+" + intervalString + ")";
                default:
                    throw new InvalidExpressionException("Unhandled operator: " + ExpressionOperator);
            }
        }
    }
}
